import base64
import json
import random
import sys
import time
from hashlib import md5
import requests
from seu_auth import seu_login
from dotenv import load_dotenv
import os
import warnings
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


def load_config():
    """
    从.env文件加载配置，如果文件不存在则创建样板文件

    Returns:
        dict: 包含所有配置信息的字典
    """
    # 默认配置
    default_config = {
        "USER_NAME": "",
        "PASSWORD": "",
        "API_USER": "",
        "API_PASS": "",
        "API_SOFTID": "",
        "API_CODETYPE": "",
        "ADVANCE_TIME": "",
        "REQUEST_INTERVAL": "",
    }

    env_path = ".env"

    # 检查.env文件是否存在
    if not os.path.exists(env_path):
        print("未找到.env配置文件，正在创建...")
        with open(env_path, "w") as f:
            f.write("# 东南大学讲座抢课系统配置文件\n\n")
            f.write("# 登录信息\n")
            f.write("USER_NAME=\n")
            f.write("PASSWORD=\n\n")
            f.write("# 超级鹰验证码识别API配置\n")
            f.write("API_USER=\n")
            f.write("API_PASS=\n")
            f.write("API_SOFTID=\n")
            f.write("API_CODETYPE=1902\n\n")
            f.write("# 抢课设置\n")
            f.write("# 提前几秒开始抢课\n")
            f.write("ADVANCE_TIME=2\n")
            f.write("# 请求间隔时间(秒)\n")
            f.write("REQUEST_INTERVAL=0.5\n")
            f.write("# 空位检查间隔时间(秒)\n")
            f.write("SPOT_CHECK_INTERVAL=1.0\n")
        print(f"已创建.env配置文件，请编辑 {os.path.abspath(env_path)} 填写账号密码")

    # 加载.env文件
    load_dotenv(env_path)

    # 读取环境变量
    config = {}
    for key in default_config:
        config[key] = os.getenv(key, default_config[key])

    return config


def get_lecture_list(username: str, password: str):
    """登录到研究生素质讲座系统，用于后续在此系统中进行其他操作。

    Args:
        username: 一卡通号
        password: 统一身份认证密码

    Returns:
        session: 登录到研究生素质讲座系统后的session
        lecture_list: 查询到的研究生素质讲座列表
        stu_cnt_arr: 二维数组，每个元素为[讲座总人数, 已预约人数]
    """
    try:
        # 登录统一身份认证平台
        service_url = "http://ehall.seu.edu.cn/gsapp/sys/jzxxtjapp/*default/index.do"
        session, redirect_url = seu_login(username, password, service_url)
        if not session:
            raise Exception("统一身份认证平台登录失败")
        if not redirect_url:
            raise Exception("获取重定向url失败")

        # 访问研究生素质讲座系统页面
        res = session.get(redirect_url, verify=False)
        if res.status_code != 200:
            raise Exception(
                f"访问研究生素质讲座系统失败[{res.status_code}, {res.reason}]"
            )

        res = session.post(
            "https://ehall.seu.edu.cn/gsapp/sys/jzxxtjapp/hdyy/queryActivityList.do?_="
            + str(int(time.time() * 1000)),
            data={
                "pageIndex": 1,
                "pageSize": 100,
                "sortField": None,
                "sortOrder": None,
            },
        )
        if res.status_code != 200:
            raise Exception(f"POST请求失败[{res.status_code}, {res.reason}]")
        lecture_list = res.json()["datas"]
        stu_cnt_arr = [[0, 0] for _ in range(len(lecture_list))]
        for i, lecture in enumerate(lecture_list):
            stu_cnt_arr[i][0] = int(lecture["HDZRS"])
            stu_cnt_arr[i][1] = int(lecture["YYRS"])
        print("获取讲座列表成功")

        return session, lecture_list, stu_cnt_arr
    except Exception as e:
        print("获取讲座列表失败，错误信息：", e)
        return None, None, None


def print_lecture_list(lecture_list: list):
    """打印讲座列表

    Args:
        lecture_list: get_lecture_list()返回的讲座列表
    """
    try:
        print("\n----------------课程列表----------------")
        for index, lecture in enumerate(lecture_list):
            print("序号：", end="")
            print(index, end=" ")
            print("课程wid：", end=" ")
            print(lecture["WID"], end="  |  ")
            print("课程名称：", end=" ")
            print(lecture["JZMC"], end="  |  ")
            print("预约开始时间：", end=" ")
            print(lecture["YYKSSJ"], end="  |  ")
            print("预约结束时间：", end=" ")
            print(lecture["YYJSSJ"], end="  |  ")
            print("活动时间：")
            print(lecture["JZSJ"])
        print("----------------课程列表end----------------\n")
    except Exception as e:
        print("打印讲座列表失败，错误信息：", e)


def fetch_lecture(hd_wid: str, ss, ver_code):
    """获取讲座信息"""
    url = "https://ehall.seu.edu.cn/gsapp/sys/jzxxtjapp/hdyy/yySave.do"
    data_json = {"HD_WID": hd_wid, "vcode": ver_code}
    form = {"paramJson": json.dumps(data_json)}

    # 只在第一次设置headers，减少每次请求的开销
    if "jzxxtjapp" not in ss.headers.get("Referer", ""):
        headers = {
            "Host": "ehall.seu.edu.cn",
            "Accept": "application/json, text/javascript, */*; q=0.01",
            "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
            "Origin": "https://ehall.seu.edu.cn",
            "Sec-Fetch-Site": "same-origin",
            "Sec-Fetch-Mode": "cors",
            "Sec-Fetch-Dest": "empty",
            "Referer": "https://ehall.seu.edu.cn/gsapp/sys/jzxxtjapp/*default/index.do",
        }
        ss.headers.update(headers)

    # 使用try/except捕获网络和JSON解析错误
    try:
        r = ss.post(url, data=form, timeout=3)  # 添加超时设置

        # 检查响应状态码
        if r.status_code != 200:
            return "HTTP_ERROR", f"HTTP错误: {r.status_code}", False

        # 检查响应内容是否为空
        if not r.text.strip():
            return "EMPTY_RESPONSE", "服务器返回空响应", False

        # 尝试解析JSON
        try:
            result = r.json()
        except json.JSONDecodeError:
            # 如果响应不是有效的JSON，尝试分析响应内容
            if "已经预约" in r.text:
                return "SUCCESS", "已经预约成功", True
            elif "验证码错误" in r.text:
                return "CAPTCHA_ERROR", "验证码错误", False
            return "JSON_ERROR", f"无效的JSON响应: {r.text[:100]}", False

        # 正常处理JSON响应
        if result.get("success", False) is not False:
            print(result)
            return result.get("code", "SUCCESS"), result.get("msg", "操作成功"), True
        return (
            result.get("code", "ERROR"),
            result.get("msg", "未知错误"),
            result.get("success", False),
        )

    except requests.exceptions.RequestException as e:
        print(f"抢课请求异常: {e}")
        return "REQUEST_ERROR", str(e), False


def get_code(ss, config, max_retries=3):
    """获取并解析验证码"""

    if not hasattr(get_code, "cache"):
        get_code.cache = {}

    retry_count = 0
    while retry_count < max_retries:
        try:
            c_url = (
                "https://ehall.seu.edu.cn/gsapp/sys/jzxxtjapp/hdyy/vcode.do?_="
                + str(int(time.time() * 1000))
            )

            # 使用较短的超时时间
            c = ss.post(c_url, timeout=2.5)

            if c.status_code != 200:
                retry_count += 1
                time.sleep(0.2)
                continue

            if not c.text:
                retry_count += 1
                time.sleep(0.2)
                continue

            try:
                c_r = c.json()
                if "result" not in c_r:
                    retry_count += 1
                    time.sleep(0.2)
                    continue

                # 计算验证码图片的哈希值作为缓存键
                img_b64 = c_r["result"].split(",")[1]
                cache_key = md5(img_b64.encode()).hexdigest()

                # 检查缓存
                if cache_key in get_code.cache:
                    return get_code.cache[cache_key], base64.b64decode(img_b64)

                # 解码图片
                c_img = base64.b64decode(img_b64)

                # 使用在线API识别验证码
                result = parse_online_verify_code(c_img, config)

                # 如果API识别成功
                if result:
                    # 缓存识别结果
                    get_code.cache[cache_key] = result
                    return result, c_img
                else:
                    # API识别失败，重试
                    retry_count += 1
                    time.sleep(0.5)  # 稍微增加等待时间
                    continue

            except json.decoder.JSONDecodeError:
                retry_count += 1
                time.sleep(0.2)
                continue

        except Exception as e:
            retry_count += 1
            time.sleep(0.2)

    # 达到最大重试次数
    return "", None


def parse_online_verify_code(img_base64, config):
    """解析验证码图片"""
    # 缓存最近的超级鹰API参数
    if not hasattr(parse_online_verify_code, "params"):
        verify_code_params = {
            "user": config["API_USER"],
            "pass": config["API_PASS"],
            "softid": config["API_SOFTID"],
            "codetype": int(config["API_CODETYPE"]),
        }
        verify_code_headers = {
            "Connection": "Keep-Alive",
            "User-Agent": "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)",
        }
        parse_online_verify_code.params = verify_code_params
        parse_online_verify_code.headers = verify_code_headers
        parse_online_verify_code.error_count = 0
        parse_online_verify_code.last_error_time = 0

    # 创建一个副本并添加base64数据
    params = parse_online_verify_code.params.copy()
    params["file_base64"] = base64.b64encode(img_base64).decode("utf-8")

    # 错误计数机制，避免API异常时反复请求造成浪费
    current_time = time.time()
    if (
        parse_online_verify_code.error_count > 5
        and current_time - parse_online_verify_code.last_error_time < 30
    ):
        # 短时间内多次错误，暂停使用API一段时间
        return None

    try:
        r = requests.post(
            url="http://upload.chaojiying.net/Upload/Processing.php",
            data=params,
            headers=parse_online_verify_code.headers,
            timeout=3,  # 添加超时设置
        )
        res = r.json()

        if res["err_no"] == 0:
            # 成功识别，重置错误计数
            parse_online_verify_code.error_count = 0
            return res["pic_str"]
        else:
            # API返回错误
            print(f"API识别错误: {res.get('err_str', '未知错误')}")
            parse_online_verify_code.error_count += 1
            parse_online_verify_code.last_error_time = current_time
            return None

    except Exception as e:
        # 请求异常
        print(f"API请求异常: {e}")
        parse_online_verify_code.error_count += 1
        parse_online_verify_code.last_error_time = current_time
        return None


def simple_grabbing_loop(
    s, user_name, password, target_index, wid, stu_cnt_arr, config
):
    """
    主循环，用于抢课

    Args:
        s: 已登录的会话对象
        user_name: 用户名
        password: 密码
        target_index: 目标讲座的索引
        wid: 讲座ID
        stu_cnt_arr: 讲座人数信息
        config: 配置信息
    """
    ##### 抢 #####
    print(time.ctime(), "开始抢课")
    print("使用超级鹰在线验证码识别服务...")

    # 获取初始验证码
    v_code, img = get_code(ss=s, config=config)

    # 如果获取失败，尝试重新登录
    if not v_code:
        print("获取验证码失败，尝试重新登录...")
        s, _, stu_cnt_arr = get_lecture_list(user_name, password)
        if not s:
            print("重新登录失败，请检查网络连接或账号密码")
            return
        v_code, img = get_code(ss=s, config=config)

    i = 1
    last_session_refresh = time.time()
    last_spot_check = time.time()  # 用于记录上一次空位检查的时间
    success_flag = False
    waiting_for_spot = False  # 标记是否正在等待空位

    # 获取请求间隔时间
    request_interval = float(config.get("REQUEST_INTERVAL", 1.0))
    # 空位检查间隔时间 (秒)
    spot_check_interval = float(config.get("SPOT_CHECK_INTERVAL", 5.0))

    try:
        while True:
            try:
                # 如果验证码为空，立即重新获取
                if not v_code:
                    print("验证码为空，重新获取...")
                    v_code, img = get_code(ss=s, config=config)
                    if not v_code:
                        print("获取验证码失败，等待后重试")
                        time.sleep(1.0)
                        continue

                # 每2分钟刷新一次会话，避免频繁重新登录
                current_time = time.time()
                if current_time - last_session_refresh > 120:
                    print("定期刷新会话...")
                    new_s, _, new_stu_cnt_arr = get_lecture_list(user_name, password)
                    if new_s and new_stu_cnt_arr:
                        s = new_s
                        stu_cnt_arr = new_stu_cnt_arr
                    else:
                        print("刷新会话失败，继续使用当前会话")
                    last_session_refresh = current_time

                # 检查讲座是否有空位
                if stu_cnt_arr[target_index][0] > stu_cnt_arr[target_index][1]:
                    # 如果之前在等待空位状态，现在有空位了，则打印提示
                    if waiting_for_spot:
                        print("\n检测到空位！尝试抢课...")
                        waiting_for_spot = False

                    # 使用单线程方式尝试
                    code, msg, success = fetch_lecture(wid, s, v_code)

                    print(f"第{i}次请求，code：{code}，msg：{msg}，success: {success}")

                    # 请求成功或已经预约
                    if success or "已经预约" in msg:
                        print("\n🎉🎉🎉 抢课成功! 🎉🎉🎉")
                        success_flag = True
                        break

                    # 验证码相关错误
                    if any(err in msg for err in ["验证码错误", "过期", "多台设备"]):
                        print("验证码问题，正在获取新验证码...")
                        # 等待更长时间，避免频繁请求
                        time.sleep(1.5 + random.random())
                        # 验证码错误，重新获取
                        v_code, img = get_code(ss=s, config=config)

                    # 人数已满错误 - 改进这部分逻辑
                    elif "人数已满" in msg:
                        # 修改状态为等待空位
                        waiting_for_spot = True

                        # 只定期打印状态，避免刷屏
                        current_time = time.time()
                        if current_time - last_spot_check >= spot_check_interval:
                            print(
                                f"当前讲座人数已满 ({stu_cnt_arr[target_index][1]}/{stu_cnt_arr[target_index][0]})"
                            )
                            print("等待空位中，定期检查讲座状态...")

                            # 刷新讲座信息
                            _, _, temp_stu_cnt_arr = get_lecture_list(
                                user_name, password
                            )
                            if temp_stu_cnt_arr:
                                # 如果有更新，检查是否有空位
                                old_count = stu_cnt_arr[target_index][1]
                                new_count = temp_stu_cnt_arr[target_index][1]
                                stu_cnt_arr = temp_stu_cnt_arr

                                if old_count > new_count:
                                    print(
                                        f"⚡ 检测到人数变化! 从 {old_count} 减少到 {new_count}，有人取消预约"
                                    )

                            last_spot_check = current_time

                        # 减少CPU占用，但保持较短的等待时间以便能快速响应空位
                        time.sleep(1.0 + random.random())

                    # 其他错误，可能需要刷新会话
                    elif any(
                        err in code
                        for err in ["JSON_ERROR", "HTTP_ERROR", "REQUEST_ERROR"]
                    ):
                        print("网络或服务器错误，正在刷新会话...")
                        time.sleep(3.0)  # 较长等待时间
                        new_s, _, new_stu_cnt_arr = get_lecture_list(
                            user_name, password
                        )
                        if new_s and new_stu_cnt_arr:
                            s = new_s
                            stu_cnt_arr = new_stu_cnt_arr
                        # 重新获取验证码
                        v_code, img = get_code(ss=s, config=config)

                    # 默认处理，增加一点等待时间
                    else:
                        time.sleep(request_interval + random.random())

                    i += 1
                else:
                    # 没有空位，进入等待状态
                    waiting_for_spot = True

                    # 定期检查空位状态
                    current_time = time.time()
                    if current_time - last_spot_check >= spot_check_interval:
                        # 刷新讲座信息
                        _, _, temp_stu_cnt_arr = get_lecture_list(user_name, password)

                        if temp_stu_cnt_arr:
                            # 如果有更新，检查是否有空位
                            old_count = (
                                stu_cnt_arr[target_index][1] if stu_cnt_arr else 0
                            )
                            new_count = temp_stu_cnt_arr[target_index][1]
                            stu_cnt_arr = temp_stu_cnt_arr
                            total = stu_cnt_arr[target_index][0]

                            # 展示当前状态
                            print(
                                f"[{time.strftime('%H:%M:%S')}] 当前讲座状态: {new_count}/{total}"
                            )

                            # 检测人数变化
                            if old_count > new_count:
                                print(
                                    f"⚡ 检测到有人取消! 人数从 {old_count} 减少到 {new_count}"
                                )
                                # 提前准备好验证码，以便有空位时立即抢
                                v_code, img = get_code(ss=s, config=config)
                            elif i % 60 == 0:  # 定期显示等待状态
                                print(
                                    f"⏳ 继续等待空位，已等待 {int(current_time - last_session_refresh)} 秒"
                                )

                        last_spot_check = current_time

                    i += 1
                    # 等待时间设置为检查间隔的1/5，保证能快速响应变化
                    time.sleep(spot_check_interval / 5.0)
                    continue

            except json.decoder.JSONDecodeError:
                print("JSON解析错误，等待3秒后重新获取会话...")
                time.sleep(3)  # 增加等待时间
                s, _, stu_cnt_arr = get_lecture_list(user_name, password)
                v_code, img = get_code(ss=s, config=config)

            except Exception as e:
                print(f"抢课错误: {e}")
                time.sleep(2.0)  # 增加等待时间
                continue

            # 合理的请求间隔，避免过于频繁
            time.sleep(request_interval + random.random())
    finally:
        if success_flag:
            print("\n恭喜！抢课成功，请登录系统查看")
        else:
            print("\n抢课结束，未能成功")


if __name__ == "__main__":
    # 加载配置
    config = load_config()

    ##### 读取账号配置 #####
    user_name = config["USER_NAME"]
    password = config["PASSWORD"]

    if not user_name or not password:
        user_name = input("请输入学号：").strip()
        password = input("请输入密码：").strip()
        # 提示用户保存账号密码到 .env
        save_to_env = input("是否保存账号密码到 .env 文件？(y/n)：").strip().lower()
        if save_to_env == "y":
            with open(".env", "r") as f:
                content = f.read()
            content = content.replace("USER_NAME=", f"USER_NAME={user_name}")
            content = content.replace("PASSWORD=", f"PASSWORD={password}")
            with open(".env", "w") as f:
                f.write(content)
            print("账号密码已保存到 .env 文件")

    ##### 登录 + 爬取课程列表 #####
    print(time.ctime(), "开始登录")
    s, lecture_list, stu_cnt_arr = get_lecture_list(user_name, password)
    if not s or not lecture_list:
        print("登录失败或获取课程列表失败，请检查网络连接和账号密码")
        sys.exit(1)

    print_lecture_list(lecture_list)

    ##### 选择课程 #####
    lecture_info = False
    while True:
        target_index = int(input("请输入课程序号：").strip())
        if target_index < 0 or target_index >= len(lecture_list):
            print("序号无效，请重新输入")
            continue
        lecture_info = lecture_list[target_index]
        wid = lecture_info["WID"]
        confirm = input(f"确认讲座名称 {lecture_info['JZMC']} (y/n)：").strip()
        if confirm == "y" or confirm == "Y":
            break

    # 从配置文件中读取提前时间，如果没有则交互输入
    advance_time = int(config.get("ADVANCE_TIME"))
    if not advance_time:
        advance_time = int(
            input(
                "请输入提前几秒开始抢（请保证本地时间准确，抢课频率受到限制，建议2秒）："
            ).strip()
        )
    else:
        print(f"将提前 {advance_time} 秒开始抢课 (配置文件设置)")

    current_time = int(time.time())
    begin_time = int(
        time.mktime(time.strptime(lecture_info["YYKSSJ"], "%Y-%m-%d %H:%M:%S"))
    )
    end_time = int(
        time.mktime(time.strptime(lecture_info["YYJSSJ"], "%Y-%m-%d %H:%M:%S"))
    )

    if current_time > end_time:
        print("抢课时间已结束，大侠请重新来过")
        sys.exit(0)

    while current_time < begin_time - advance_time:
        current_time = int(time.time())
        print("等待{}秒".format(begin_time - advance_time - current_time))
        if begin_time - advance_time - current_time < 5:
            time.sleep(0.51)
        else:
            time.sleep(1)

    simple_grabbing_loop(
        s=s,
        user_name=user_name,
        password=password,
        target_index=target_index,
        wid=wid,
        stu_cnt_arr=stu_cnt_arr,
        config=config,
    )
